<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>15.2-parser 的实现原理与状态机</title>
</head>
<body>
    
</body>
    <script>
        /**
         * Vue.js 模板编译器的基本结构和工作流程
         * - 用来将模板字符串解析为模板 AST 的解析器（parser）​；
         * - 用来将模板 AST 转换为 JavaScript AST 的转换器（transformer）​；
         * - 用来根据 JavaScript AST 生成渲染函数代码的生成器（generator）​。
         * 
         * 详细讨论解析器 parser 的实现原理
         * - 解析器的入参是字符串模板，
         *    解析器会逐个读取字符串模板中的字符，
         *    并根据一定的规则将整个字符串切割为一个个 Token
         * 
         * 有限状态自动机
         * - 所谓 “有限状态”​，就是指有限个状态
         * - “自动机” 意味着随着字符的输入，解析器会自动地在不同状态间迁移
         * - 拿上面的模板来说，当我们分析这段模板字符串时，parse 函数会逐个读取字符
         * - 状态机会有一个初始状态，我们记为 “初始状态 1”​。
         * 
         * WHATWG 发布的关于浏览器解析 HTML 的规范
         * - 在 “初始状态”​（Data State）下，当遇到字符 < 时，状态机会迁移到tag open state，即 “标签开始状态”​。
         *    如果遇到字符 < 以外的字符，规范中也都有对应的说明，应该让状态机迁移到怎样的状态
         * - Vue.js 的模板毕竟是类 HTML 的实现，因此，尽可能按照规范来做，不会有什么坏处
         * - 规范中已经定义了非常详细的状态迁移过程
         * 
         * 正则表达式的本质就是有限自动机。当你编写正则表达式的时候，其实就是在编写有限自动机。
         * 
        */
        // 定义状态机的状态
        const State = {// 新增
            initial: 1,    // 初始状态
            tagOpen: 2,    // 标签开始状态
            tagName: 3,    // 标签名称状态
            text: 4,       // 文本状态
            tagEnd: 5,     // 结束标签状态
            tagEndName: 6  // 结束标签名称状态
        }
        
        // 一个辅助函数，用于判断是否是字母
        function isAlpha(char) {// 新增
            return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
        }

        // 接收模板字符串作为参数，并将模板切割为 Token 返回
        function tokenize(str) { // 新增 tokenize 函数
            // 状态机的当前状态：初始状态
            let currentState = State.initial
            // 用于缓存字符
            const chars = []
            // 生成的 Token 会存储到 tokens 数组中，并作为函数的返回值返回
            const tokens = []
            // 使用 while 循环开启自动机，只要模板字符串没有被消费尽，自动机就会一直运行
            while(str) {
                // 查看第一个字符，注意，这里只是查看，没有消费该字符
                const char = str[0]
                // switch 语句匹配当前状态
                switch (currentState) {
                    // 状态机当前处于初始状态
                    case State.initial:
                        // 遇到字符 <
                        if (char === '<') {
                            // 1. 状态机切换到标签开始状态
                            currentState = State.tagOpen
                            // 2. 消费字符 <
                            str = str.slice(1)
                        } else if (isAlpha(char)) {
                            // 1. 遇到字母，切换到文本状态
                            currentState = State.text
                            // 2. 将当前字母缓存到 chars 数组
                            chars.push(char)
                            // 3. 消费当前字符
                            str = str.slice(1)
                        }
                        break
                    // 状态机当前处于标签开始状态
                    case State.tagOpen:
                        if (isAlpha(char)) {
                            // 1. 遇到字母，切换到标签名称状态
                            currentState = State.tagName
                            // 2. 将当前字符缓存到 chars 数组
                            chars.push(char)
                            // 3. 消费当前字符
                            str = str.slice(1)
                        } else if (char === '/') {
                            // 1. 遇到字符 /，切换到结束标签状态
                            currentState = State.tagEnd
                            // 2. 消费字符 /
                            str = str.slice(1)
                        }
                        break
                    // 状态机当前处于标签名称状态
                    case State.tagName:
                        if (isAlpha(char)) {
                            // 1. 遇到字母，由于当前处于标签名称状态，所以不需要切换状态，
                            // 但需要将当前字符缓存到 chars 数组
                            chars.push(char)
                            // 2. 消费当前字符
                            str = str.slice(1)
                        } else if (char === '>') {
                            // 1.遇到字符 >，切换到初始状态
                            currentState = State.initial
                            // 2. 同时创建一个标签 Token，并添加到 tokens 数组中
                            // 注意，此时 chars 数组中缓存的字符就是标签名称
                            tokens.push({
                                type: 'tag',
                                name: chars.join('')
                            })
                            // 3. chars 数组的内容已经被消费，清空它
                            chars.length = 0
                            // 4. 同时消费当前字符 >
                            str = str.slice(1)
                        }
                        break
                    // 状态机当前处于文本状态
                    case State.text:
                        if (isAlpha(char)) {
                            // 1. 遇到字母，保持状态不变，但应该将当前字符缓存到 chars 数组
                            chars.push(char)
                            // 2. 消费当前字符
                            str = str.slice(1)
                        } else if (char === '<') {
                            // 1. 遇到字符 <，切换到标签开始状态
                            currentState = State.tagOpen
                            // 2. 从 文本状态 --> 标签开始状态，此时应该创建文本 Token，并添加到 tokens 数组
                            // 注意，此时 chars 数组中的字符就是文本内容
                            tokens.push({
                                type: 'text',
                                content: chars.join('')
                            })
                            // 3. chars 数组的内容已经被消费，清空它
                            chars.length = 0
                            // 4. 消费当前字符
                            str = str.slice(1)
                        }
                        break
                    // 状态机当前处于标签结束状态
                    case State.tagEnd:
                        if (isAlpha(char)) {
                            // 1. 遇到字母，切换到结束标签名称状态
                            currentState = State.tagEndName
                            // 2. 将当前字符缓存到 chars 数组
                            chars.push(char)
                            // 3. 消费当前字符
                            str = str.slice(1)
                        }
                        break
                    // 状态机当前处于结束标签名称状态
                    case State.tagEndName:
                        if (isAlpha(char)) {
                            // 1. 遇到字母，不需要切换状态，但需要将当前字符缓存到 chars 数组
                            chars.push(char)
                            // 2. 消费当前字符
                            str = str.slice(1)
                        } else if (char === '>') {
                            // 1. 遇到字符 >，切换到初始状态
                            currentState = State.initial
                            // 2. 从 结束标签名称状态 --> 初始状态，应该保存结束标签名称 Token
                            // 注意，此时 chars 数组中缓存的内容就是标签名称
                            tokens.push({
                                type: 'tagEnd',
                                name: chars.join('')
                            })
                            // 3. chars 数组的内容已经被消费，清空它
                            chars.length = 0
                            // 4. 消费当前字符
                            str = str.slice(1)
                        }
                        break
                }
            }

            // 最后，返回 tokens
            return tokens
        }

        // 测试
        const tokens = tokenize('<div>hel<p>vue</p>lo</div>');
        console.log(tokens);
    </script>
    

</html>